/**
 * @method Method              常用开发解决方案
 * @author Lkx
 *
 * @method url_data            获取浏览器地址参数
 *
 * @method str_rep             替换指定字符
 * @method str_ins             在某[字符/下标]之后插入
 * @method str_que             获取指定字符第一次出现[前/后]的所有字符
 * @method str_ellipsis        固定文本长度且超出隐藏
 * @method str_random          生成随机字符串
 * @method str_random_color    生成随机颜色
 * @method str_form            表单验证
 * @method str_del_space       删除[字符串/对象]内所有空格
 * @method str_html_rep        转义所有HTML标签的括号
 * @method str_html_del        去掉所有HTML标签
 * @method str_is_json         判断当前字符串是否为JSON格式
 * @method str_copy            复制指定文本至剪贴板
 * @method str_count_lines     获取当前文本的行数
 *
 * @method data                判断数据类型及空值状态
 * @method data_defense        主动防御
 * @method data_agent          判断当前终端设备
 * @method data_script         动态加载脚本文件
 * @method data_compare        对比数据是否相等
 *
 * @method arr_sort            数组排序[冒泡排序]
 * @method arr_obj_sort        数组对象排序
 * @method arr_obj_contain     数组对象中是否包含某个键值
 * @method arr_count           元素在数组中出现的次数
 * @method arr_del_rep         数组去重
 * @method arr_del_ele         删除数组中某个元素
 * @method arr_intersect       对比数组取出交集
 * @method arr_sku             生成SKU
 *
 * @method obj_deep_copy       对象深拷贝
 *
 * @method sto_cache_set       设置本地缓存
 * @method sto_cache_get       获取本地缓存
 * @method sto_cache_del       删除本地缓存
 * @method sto_cookie_set      设置COOKIE
 * @method sto_cookie_get      获取COOKIE
 * @method sto_cookie_del      删除COOKIE
 * @method sto_history_set     设置历史记录
 * @method sto_history_get     获取历史记录
 * @method sto_history_del     删除历史记录
 *
 * @method date_get            获取当前时间戳
 * @method date_format_seconds 格式化秒[转天时分秒]
 *
 * @method num_random          生成随机数字
 * @method num_decimal         保留任意位小数
 * @method num_decimal_pla     获取小数点位数
 * @method num_acc_add         浮点运算[加法]
 * @method num_acc_sub         浮点运算[减法]
 * @method num_acc_mul         浮点运算[乘法]
 * @method num_acc_div         浮点运算[除法]
 * @method num_money           金钱格式
 * @method num_bank            银行卡号格式
 * @method num_percent         获取百分比
 * @method num_zero_fill       在某数字前补零
 * @method num_ani             数字变换动画
 * @method num_zoom            等比缩放
 *
 * @method evt_stop            阻止事件冒泡
 * @method evt_font_adapt      移动端尺寸自适应
 * @method evt_scroll          移动滚动条位置
 * @method evt_location        获取[鼠标/手势]事件位置
 * @method evt_direction       获取[鼠标/手势]滑动方向
 */

(function (factory) {

  if (typeof exports === "object" && typeof module !== "undefined") {
    module.exports = factory();
  } else {
    window.Kxui.method = window.kxui.method = factory();
  }

}(function () {

  "use strict";

  const win = window;
  const doc = win.document;
  const Kxui = win.Kxui;

  // 全局错误样式
  // 通过主入口注册，达到全模块统一错误输出
  const Kxui_throws_style = win.Kxui_throws_style;

  // 移动滚动条位置计时器
  // 防止多次重复点击而生成的多个计时器
  let evt_scroll_timer = "";

  // 用于数字变换动画储存历史可用数据
  let num_ani_list = [];

  /**
   * @method 异常抛出
   * @param  {number} num       抛出错误文案的编号
   * @param  {string} name      抛出错误的方法
   * @param  {string} parameter 抛出错误的参数
   * @param  {string} info      其他说明信息
   */
  const throws = (num, name, parameter, info) => {
    if (Kxui.configuration.debug) {
      const nums = {};
      nums[0] = `配置参数 {${parameter}} 为空或数据类型错误，正确数据类型应为 {${info}}`;
      nums[1] = `配置参数 {${parameter}} 传入数据错误${info ? `，其值${info}` : ""}`;
      console.error(`%cKxui.method.${name}%c (${Kxui.about.version})\n%c${nums[num]}，请检查修改。`, Kxui_throws_style[0], Kxui_throws_style[1], Kxui_throws_style[2]);
    }
  };

  /**
   * @method 判断数据类型及空值状态
   * @param  {object}  data      需要参与判断的对象
   * @param  {string}  data_type 需要判断的数据类型
   * @param  {boolean} strict    是否开启严格模式[是(true)/否(false)]
   *
   * data_processing(data, data_type, strict)
   * 当判断类型为 object 时，若数据为 array 将不进行通过
   *
   * 非严格模式下差异:
   * 当判断类型为 number 时，string 形态的 number 将会被通过，方便容错率
   * 当判断对象或数组时，对象和数组为空将会通过
   *
   * 严格模式下差异:
   * 当判断类型为 number 时，string 形态的 number 将不会被通过
   * 当判断对象或数组时，对象和数组必须不能为空
   */
  const data_processing = (data, data_type, strict) => {
    const is_strict = typeof strict === "boolean" ? strict : false;
    const is_obj_arr = is_strict ? JSON.stringify(data) === "{}" || (data && data.length === 0) : false;
    if (typeof data === "undefined" || data === null || data === "" || is_obj_arr) {
      return false;
    } else if (data_type) {
      const is_data_type = typeof data_type === "string";
      if (is_data_type) {
        const is_array =  Object.prototype.toString.call(data) === "[object Array]";
        const data_type_large = data_type.toUpperCase();
        const data_type_of = (typeof data).toUpperCase();

        if (data_type_large === "NUMBER") {
          return is_strict ? data_type_of === data_type_large : data_type_of === "STRING" ? !isNaN(data) : data_type_of === data_type_large;
        } if (data_type_large === "ARRAY") {
          return is_array;
        } else {
          return is_array ? false : data_type_of === data_type_large;
        }
      } else {
        throws(0, "data", "data_type", "string");
        return false;
      }
    } else {
      return true;
    }
  };

  /**
   * @method 浮点运算综合方法
   * @param  {array}  arg  参与计算的数字数组
   * @param  {string} type 计算方式
   * @return {number}      计算结果
   */
  const num_operation = (arg, type) => {
    let value = arg[0];

    // 运算对象
    // 根据不同运算调用不同方法
    const operation_type = {

      // 浮点运算[加法]
      add(a, b) {
        let r1, r2, m, c;
        try { r1 = a.toString().split(".")[1].length; } catch (e) { r1 = 0; }
        try { r2 = b.toString().split(".")[1].length; } catch (e) { r2 = 0; }
        c = Math.abs(r1 - r2);
        m = Math.pow(10, Math.max(r1, r2));
        if (c > 0) {
          const cm = Math.pow(10, c);
          if (r1 > r2) {
            a = Number(a.toString().replace(".", ""));
            b = Number(b.toString().replace(".", "")) * cm;
          } else {
            a = Number(a.toString().replace(".", "")) * cm;
            b = Number(b.toString().replace(".", ""));
          }
        } else {
          a = Number(a.toString().replace(".", ""));
          b = Number(b.toString().replace(".", ""));
        }
        return (a + b) / m;
      },

      // 浮点运算[减法]
      sub(a, b) {
        let r1, r2, m, n, result;
        try { r1 = a.toString().split(".")[1].length; } catch (e) { r1 = 0; }
        try { r2 = b.toString().split(".")[1].length; } catch (e) { r2 = 0; }
        m = Math.pow(10, Math.max(r1, r2));
        n = (r1 >= r2) ? r1 : r2;
        result = (a * m - b * m) / m;
        return Number(result.toFixed(n));
      },

      // 浮点运算[乘法]
      mul(a, b) {
        let m = 0;
        let s1 = a.toString();
        let s2 = b.toString();
        try { m += s1.split(".")[1].length; } catch (e) {}
        try { m += s2.split(".")[1].length; } catch (e) {}
        return Number(Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m));
      },

      // 浮点运算[除法]
      div(a, b) {
        let t1 = 0;
        let t2 = 0;
        let r1, r2;
        try { t1 = a.toString().split(".")[1].length; } catch (e) {}
        try { t2 = b.toString().split(".")[1].length; } catch (e) {}
        r1 = Number(a.toString().replace(".", ""));
        r2 = Number(b.toString().replace(".", ""));
        return Number((r1 / r2) * Math.pow(10, t2 - t1));
      }
    };

    for (let i = 0; i < arg.length - 1; i++) {
      if (data_processing(arg[i], "number") && data_processing(arg[i + 1], "number")) {
        value = data_processing(value, "number") ? value : arg[i];
        value = operation_type[type](value, arg[i + 1]);
      }
    }

    return value;
  };

  class Method {

    constructor() {
      this.name = "Method";
      this.info = "Common Development Solutions";
    }

    // ============================================ 浏览器地址(开始) ============================================

    /**
     * @method 获取浏览器地址参数
     * @param  {string} data 需要拿取的参数名
     * @return {string}      返回参数值或空
     *
     * Method.url_data(data)
     */
    url_data(data) {
      if (this.data(data, "string")) {
        const result = win.location.search.match(new RegExp(`[\\?\\&]${data}=([^\\&]+)`, "i"));
        return ((!result || result.length < 1) ? "" : decodeURI(result[1]));
      }
      throws(0, "url_data", "data", "string");
    }

    // ============================================ 浏览器地址(结束) ============================================

    // ============================================ 字符串操作(开始) ============================================

    /**
     * @method 替换指定字符
     * @param  {string}        str 需要操作的字符串
     * @param  {string/number} app 替换方式[字符/下标]
     * @param  {string}        rep 替换之后的字符[默认为空]
     * @return {string}            返回替换后的字符串
     *
     * Method.str_rep(str, app, rep)
     * 当需要通过字符查找时，app 应为 string 类型，并且定位至需要替换的字符
     * 如果需用下标进行查询，则 app 应为 number 类型，需注意下标是从 0 开始计算
     */
    str_rep(str, app, rep) {
      const is_str = this.data(str, "string");
      const is_app = this.data(app, "string") || this.data(app, "number");
      const is_rep = !this.data(rep, "string") && !this.data(rep, "number");
      if (is_str && is_app) {
        if (rep && is_rep) {
          throws(0, "str_rep", "rep", "string/number");
          return str;
        } else if (this.data(app, "string")) {
          return str.split(app).join(rep || "");
        } else {
          const front = str.substr(0, app);
          const after = str.substr(app + 1, str.length);
          return front + (rep || "") + after;
        }
      }
      throws(0, "str_rep", is_str ? "app" : "str", is_str ? "string/number" : "string");
    }

    /**
     * @method 在某[字符/下标]之后插入
     * @param  {string}        str   需要操作的字符串
     * @param  {string/number} after 在某字符之后或某个下标之后
     * @param  {string}        app   需要插入的字符
     * @return {string}              返回插入后的字符串
     *
     * Method.str_ins(str, after, app)
     * 当通过下标进行插入时，需注意下标是从 0 开始计算
     */
    str_ins(str, after, app) {
      const is_str = this.data(str, "string");
      const is_after = this.data(after, "string") || this.data(after, "number");
      const is_app = this.data(app, "string") || this.data(app, "number");
      if (is_str && is_after && is_app) {
        const S = str.split(after);
        if (this.data(after, "string")) {
          S[1] = app + S[1];
          return S.join(after);
        } else {
          return (str.slice(0, after + 1) + app + str.slice(after + 1));
        }
      }
      throws(0, "str_ins", is_str ? is_after ? "app" : "after" : "str", is_str ? "string/number" : "string");
    }

    /**
     * @method 获取指定字符第一次出现[前/后]的所有字符
     * @param  {string}  str 需要操作的字符串
     * @param  {string}  app 需要指定的字符
     * @param  {boolean} pos 获取方向[前(true)/后(false)]
     * @return {string}      返回前后字符串或空
     *
     * Method.str_que(str, app, pos)
     * 当有重复或循环的数据时，通常只会取第一次出现的位置
     */
    str_que(str, app, pos) {
      const is_str = this.data(str, "string");
      const is_app = this.data(app, "string");
      if (is_str && is_app) {
        const index = str.indexOf(app);
        const app_length = app.length;
        const position = this.data(pos, "boolean") ? pos : true;
        return position ? str.substring(0, index) : str.substring(index + app_length, str.length);
      }
      throws(0, "str_que", is_str ? "app" : "str", "string");
    }

    /**
     * @method 固定文本长度且超出隐藏
     * @param  {string}  str  需要操作的字符串
     * @param  {number}  num  需要保留的位数
     * @param  {boolean} type 是否使用字节判断[是(true)/否(false)]
     * @return {string}       返回新的文本
     *
     * Method.str_ellipsis(str, num, type)
     */
    str_ellipsis(str, num, type) {
      const is_str = this.data(str, "string");
      const is_num = this.data(num, "number");
      if (is_str && is_num) {
        const is_type = (this.data(type, "boolean") ? type : false);
        const splicing_ellipsis = (output) => { return output.length === str.length ? output : `${output}...`; };
        if (is_type) {
          let bytes_count = 0;
          for (let i = 0; i < str.length; i++) {
            const c = str.charAt(i);
            if (/^[\u0000-\u00ff]$/.test(c)) {
              bytes_count += 1;
            } else {
              bytes_count += 2;
            }

            if (bytes_count > num) {
              return splicing_ellipsis(str.substr(0, i));
            } else if (bytes_count === num) {
              return splicing_ellipsis(str.substr(0, i + 1));
            }
          }
        } else {
          str = splicing_ellipsis(str.substring(0, num));
        }
        return str;
      }
      throws(0, "str_ellipsis", is_str ? "num" : "str", is_str ? "number" : "string");
    }

    /**
     * @method 生成随机字符串
     * @param  {number} len 随机字符串长度[默认(8)位]
     * @return {string}     返回随机结果
     *
     * Method.str_random(len)
     */
    str_random(len) {
      len = +len || 8;
      let str = "";
      for (str = ""; str.length < len; str += Math.random().toString(36).substr(2));
      return str.substr(0, len);
    }

    /**
     * @method 生成随机颜色
     * @param  {boolean} type 颜色类型[二进制(true)/RGB(false)]
     * @return {string}       返回随机结果
     *
     * Method.str_random_color(type)
     */
    str_random_color(type) {
      const Type = (this.data(type, "boolean") ? type : true);
      if (Type) {
        return `#${Math.random().toString(16).substring(2).substr(0, 6)}`;
      } else {
        return `rgb(${this.num_random()}, ${this.num_random()}, ${this.num_random()})`;
      }
    }

    /**
     * @method 表单验证
     * @param  {string} str 需要验证的数据
     * @param  {string} reg 验证规则
     *
     * empty                不能为空
     * num                  不允许存在小数的数字
     * decimal              允许存在小数的数字
     * money                非 0 金钱
     * letter               字母
     * phone                手机号(中国)
     * chinese              中文
     * color                十六进制颜色
     * id                   身份证
     * passport             护照(包含香港、澳门)
     * url                  链接地址
     * md5                  有效的md5格式(32位)
     * email                邮箱
     * IPv4                 IPv4 地址
     * IPv6                 IPv6 地址
     *
     * @return {boolean}    根据验证情况进行返回布尔值
     *
     * Method.str_form(str, reg)
     */
    str_form(str, reg) {
      const is_reg = this.data(reg, "string");
      if (is_reg) {
        const regs = {};
        regs.empty = /\S/;
        regs.num = /^\d+$/;
        regs.decimal = /^\d+(\.\d+)?$/;
        regs.money = /(^(([1-9]\d*)|([0-9]\d*\.\d?[1-9]{1}))$)/;
        regs.letter = /^[a-zA-Z]+$/;
        regs.phone = /^1[3456789]\d{9}$/;
        regs.chinese = /^[\u4e00-\u9fa5]+$/;
        regs.color = /^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/;
        regs.id = /^\d{6}(18|19|20)?\d{2}(0[1-9]|1[12])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i;
        regs.passport = /(^[EeKkGgDdSsPpHh]\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\d{7}$)/;
        regs.url = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&"\*\+,;=.]+$/;
        regs.md5 = /^[a-f0-9]{32}$/;
        regs.email = /^[a-z0-9]+([._\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/;
        regs.IPv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
        regs.IPv6 = /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i;
        return (!!((regs[reg] && regs[reg].test(str))));
      }
      throws(0, "str_form", "reg", "string");
    }

    /**
     * @method 删除[字符串/对象]内所有空格
     * @param  {string/array/object} ope 需要操作的字符串/数组/对象
     * @return {string/array/object}     返回去掉空格的字符串/对象
     *
     * Method.str_del_space(ope)
     */
    str_del_space(ope) {
      if (this.data(ope, "string") || this.data(ope, "array") || this.data(ope, "object")) {
        if (this.data(ope, "string")) {
          return ope.replace(/[ ]/g, "");
        } else {
          for (let prop in ope) {
            if (this.data(ope[prop], "object") && Array.isArray(ope[prop])) {
              let list = ope[prop];
              for (let i = 0; i < list.length; i++) {
                list[i] = this.str_del_space(list[i]);
              }
            } else if (this.data(ope[prop], "object") && (ope[prop]) instanceof Object) {
              ope[prop] = this.str_del_space(ope[prop]);
            } else if (this.data(ope[prop], "string")) {
              if (prop !== "FormMeta") {
                ope[prop] = ope[prop].replace(/[ ]/g, "");
              }
            }
          }
          return ope;
        }
      }
      throws(0, "str_del_space", "ope", "string/array/object");
    }

    /**
     * @method 转义所有HTML标签的括号
     * @param  {string} str 完整的HTML标签
     * @return {string}     返回字符串HTML标签
     *
     * Method.str_html_rep(str)
     * 将原有 HTML 尖括号进行转义
     */
    str_html_rep(str) {
      if (this.data(str, "string")) {
        while (str.indexOf(">") >= 0 || str.indexOf("<") >= 0) {
          str = str.replace("<", "&lt;").replace(">", "&gt;");
        }
        return str;
      }
      throws(0, "str_html_rep", "str", "string");
    }

    /**
     * @method 去掉所有HTML标签
     * @param  {string} str 完整的HTML标签
     * @return {string}     返回去掉所有HTML标签的字符串
     *
     * Method.str_html_del(str)
     */
    str_html_del(str) {
      if (this.data(str, "string")) {
        return str.replace(/<[^>]+>/g, "");
      }
      throws(0, "str_html_del", "str", "string");
    }

    /**
     * @method 判断当前字符串是否为JSON格式
     * @param  {string} str 参与判断的字符串
     * @return {boolean}    返回是否为JSON格式的布尔值
     *
     * Method.str_is_json(str)
     */
    str_is_json(str) {
      if (this.data(str, "string")) {
        try {
          const obj = JSON.parse(str);
          if (obj && this.data(obj, "array") || this.data(obj, "object")){
            return true;
          } else {
            return false;
          }
        } catch(e) {
          return false;
        }
      }
      throws(0, "str_is_json", "str", "string");
    }

    /**
     * @method 复制指定文本至剪贴板
     * @param  {string}  text 需要复制的文本内容
     * @return {boolean}      是否成功
     *
     * Method.str_copy(text)
     */
    str_copy(text) {
      if (this.data(text, "string")) {
        const input = doc.createElement("input");
        const body = doc.getElementsByTagName("body")[0];
        body.appendChild(input);
        input.value = text;
        input.select();
        input.setSelectionRange(0, input.value.length);
        doc.execCommand("Copy");
        body.removeChild(input);
        return true;
      }
      throws(0, "str_copy", "text", "string");
    }

    /**
     * @method 获取当前文本的行数
     * @param  {string/object} dom 获取节点
     *
     * Method.str_count_lines(dom)
     */
    str_count_lines(dom) {
      const ele = this.data(dom, "string") ? doc.querySelector(dom) : this.data(dom, "object") ? dom[0] || dom : false;
      if (ele) {
        const styles = win.getComputedStyle(ele, null);
        const font_size = parseInt(styles.fontSize);
        const height = parseInt(styles.height);

        let line_height = parseInt(styles.lineHeight);
        if (!line_height) {
          line_height = this.num_acc_add(font_size, 4);
        }

        return Math.floor(this.num_acc_div(height, line_height));
      }
      throws(0, "str_count_lines", "dom", "string/object");
    }

    // ============================================ 字符串操作(结束) ============================================

    // ============================================= 数据操作(开始) =============================================

    /**
     * @method 判断数据类型及空值状态
     * @param  {object}  data      需要参与判断的对象
     * @param  {string}  data_type 需要判断的数据类型
     * @param  {boolean} strict    是否开启严格模式[是(true)/否(false)]
     *
     * Method.data(data, data_type, strict)
     * 详细说明见 data_processing 方法
     */
    data(data, data_type, strict) {
      return data_processing(data, data_type, strict);
    }

    /**
     * @method 主动防御
     * @param  {object} obj  需要操作的对象
     * @param  {array}  arr  属性列表层级
     * @param  {string} type 数据类型限制
     * @return {*}           如果正常访问到，则返回对应的值，否则返回空字符串或数据类型限制所规定的类型
     *
     * 对于我们操作的数据，尤其是由 API 接口返回的，时常会有一个很复杂的深层嵌套的数据结构
     * 为了代码的健壮性，很多时候需要对每一层访问都作空值判断，代码看起来相当不美观，因此提供了一个非常简洁明了的原生的方式
     * 限制了数据类型，若是为空或返回值不匹配，都将返回对应数据类型的空
     * 只有当数据类型限制为 number/string/boolean 时，才会进行尝试转换
     * 当转换 boolean 时，若是为空数组或对象，都将会转换为 false
     *
     * let obj = {data: "test", list: []}
     * Method.data_defense(obj, ["list"])
     *
     * let obj = {a: "1", b: {c: 2}}
     * Method.data_defense(obj, ["b", "c"])
     *
     * 可以不传属性列表层级直接进行数据类型防御
     * let obj = {}
     * Method.data_defense(obj, "object")
     */
    data_defense(obj, arr, type) {
      const is_arr = this.data(arr, "array");
      const is_type = is_arr ? this.data(type, "string") : this.data(arr, "string");

      // 数据获取
      let data = "";
      if (is_arr) {
        data = arr.reduce((xs, x) => {
          return xs && this.data(xs[x]) ? xs[x] : "";
        }, obj);
      } else {
        data = obj || "";
      }

      // 类型限制
      if (is_type) {
        const give_type = (is_arr ? type : arr).toUpperCase();
        const is_array =  Object.prototype.toString.call(data) === "[object Array]";
        const is_data_type = is_array ? "ARRAY" : (typeof data).toUpperCase();

        if (give_type !== is_data_type) {
          switch (give_type) {
            case "STRING":
              const data_string = String(data);
              data = data_string === "[object Object]" ? "" : data_string ;
              break;
            case "NUMBER":
              const data_number = Number(data);
              data = isNaN(data_number) ? 0 : data_number;
              break;
            case "ARRAY":
              data = [];
              break;
            case "OBJECT":
              data = {};
              break;
            case "BOOLEAN":
              data = this.data(data, is_data_type, true);
              break;
          }
        }
      }
      return data;
    }

    /**
     * @method 判断当前终端设备
     * @return {object} 各个终端设备布尔值
     *
     * Method.data_agent()
     */
    data_agent() {
      const ua = navigator.userAgent;
      const is_windows_phone = /(?:Windows Phone)/.test(ua);
      const is_symbian = /(?:SymbianOS)/.test(ua) || is_windows_phone;
      const is_android = /(?:Android)/.test(ua);
      const is_fireFox = /(?:Firefox)/.test(ua);
      const is_tablet = /(?:iPad|PlayBook)/.test(ua) || (is_android && !/(?:Mobile)/.test(ua)) || (is_fireFox && /(?:Tablet)/.test(ua));
      const is_ios = /(?:iPhone)/.test(ua) && !is_tablet;
      const is_pc = !is_ios && !is_android && !is_symbian;
      return {
        is_android: is_android,
        is_ios: is_ios,
        is_pc: is_pc,
        is_tablet: is_tablet
      };
    }

    /**
     * @method 动态加载脚本文件
     * @param  {string}   src      脚本文件路径
     * @param  {function} callback 加载完成后回调
     *
     * Method.data_script(src, callback)
     */
    data_script(src, callback) {
      const is_src  = this.data(src, "string");
      const is_callback = this.data(callback, "function");
      if (is_src && is_callback) {
        const id = this.str_rep(this.str_rep(src, "."), "/");
        const id_scropt = doc.getElementById(id);
        if (!id_scropt) {
          const body = doc.getElementsByTagName("body")[0];
          const script = doc.createElement("script");
          const wait_end = () => {
            callback(true);
          };

          script.type = "text/javascript";
          script.async = "async";
          script.src = src;
          script.id = id;

          body.appendChild(script);
          if (script.readyState) {
            script.onreadystatechange = () => {
              if (script.readyState === "complete" || script.readyState === "loaded") {
                script.onreadystatechange = null;
                wait_end();
              }
            };
          } else {
            script.onload = () => { wait_end(); };
          }
        } else {
          callback(false);
        }
      } else {
        throws(0, "data_script", is_src ? "callback" : "src", is_src ? "function" : "string");
      }
    }

    /**
     * @method 对比数据是否相等
     * @param  {*}       data 参与对比的数据，理论上可以无限多个
     * @return {boolean}      是否相等布尔值
     *
     * Method.data_compare(data1, data2, ……)
     */
    data_compare() {
      let i, l, leftChain, rightChain;
      const compare2Objects = (x, y) => {
        let p;

        if (isNaN(x) && isNaN(y) && typeof x === "number" && typeof y === "number") { return true; }
        if (x === y) { return true; }
        if ((typeof x === "function" && typeof y === "function") || (x instanceof Date && y instanceof Date) || (x instanceof RegExp && y instanceof RegExp) || (x instanceof String && y instanceof String) || (x instanceof Number && y instanceof Number)) { return x.toString() === y.toString(); }
        if (!(x instanceof Object && y instanceof Object)) { return false; }
        if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) { return false; }
        if (x.constructor !== y.constructor) { return false; }
        if (x.prototype !== y.prototype) { return false; }
        if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) { return false; }
        for (p in y) {
          if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
            return false;
          } else if (typeof y[p] !== typeof x[p]) {
            return false;
          }
        }

        for (p in x) {
          if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
            return false;
          } else if (typeof y[p] !== typeof x[p]) {
            return false;
          }

          switch (typeof(x[p])) {
            case "object":
            case "function":
              leftChain.push(x);
              rightChain.push(y);

              if (!compare2Objects(x[p], y[p])) { return false; }
              leftChain.pop();
              rightChain.pop();
              break;
            default:
              if (x[p] !== y[p]) { return false; }
              break;
          }
        }
        return true;
      };

      if (arguments.length < 1) { return true; }
      for (i = 1, l = arguments.length; i < l; i++) {
        leftChain = [];
        rightChain = [];
        if (!compare2Objects(arguments[0], arguments[i])) { return false; }
      }
      return true;
    }

    // ============================================= 数据操作(结束) =============================================

    // ============================================= 数组操作(开始) =============================================

    /**
     * @method 数组排序[冒泡排序]
     * @param  {array}   arr 需要操作的数组
     * @param  {boolean} dir 是否使用正序[正序(true)/倒序(false)]
     * @return {array}       返回排序后的数组
     *
     * Method.arr_sort(arr, dir)
     */
    arr_sort(arr, dir) {
      if (this.data(arr, "array")) {
        let temp = [];
        let Dir = this.data(dir, "boolean") ? dir : true;
        for (let i = 0; i < arr.length - 1; i++) {
          let bool = true;
          for (let j = 0; j < arr.length - 1 - i; j++) {
            let condition = (Dir ? arr[j] > arr[j + 1] : arr[j] < arr[j + 1]);
            if (condition) {
              temp = arr[j];
              arr[j] = arr[j + 1];
              arr[j + 1] = temp;
              bool = false;
            }
          }
          if (bool) { break; }
        }
        return arr;
      }
      throws(0, "arr_sort", "arr", "array");
    }

    /**
     * @method 数组对象排序
     * @param  {array}   arr 需要操作的数组对象
     * @param  {string}  bas 排序依据的键值对键名
     * @param  {boolean} dir 是否使用正序[正序(true)/倒序(false)]
     * @return {object}      返回排序后的数组对象
     *
     * let arr = [{a: 3}, {a: 2}, {a: 1}]
     * Method.arr_obj_sort(arr, "a")
     */
    arr_obj_sort(arr, bas, dir) {
      const is_arr = this.data(arr, "array");
      const is_bas = this.data(bas, "string");
      if (is_arr && is_bas) {
        const Dir = this.data(dir, "boolean") ? dir : true;
        const sortBas = (a, b) => {
          return Dir ? a[bas] - b[bas] : b[bas] - a[bas];
        };
        return arr.sort(sortBas);
      }
      throws(0, "arr_obj_sort", is_arr ? "bas" : "arr", is_arr ? "string" : "array");
    }

    /**
     * @method 数组对象中是否包含某个键值
     * @param  {array}   arr 需要操作的数组对象
     * @param  {string}  key 需要查找包含的键
     * @param  {*}       val 需要查找包含的键所对应的值
     * @return {boolean}     返回是否包含结果
     *
     * let arr = [{a: 1}, {a: 2}]
     * Method.arr_obj_contain(arr, a)
     *
     * let arr = [{a: 1}, {a: 2}, {a: 3}]
     * Method.arr_obj_contain(arr, a, 1)
     */
    arr_obj_contain(arr, key, val) {
      const is_arr = this.data(arr, "array");
      const is_key = this.data(key, "string");
      if (is_arr && is_key) {
        const is_val = this.data(val);
        const length = arr.length;
        for(let i = 0; i < length; i++) {
          if (arr[i].hasOwnProperty(key)) {
            if (is_val && this.data_compare(arr[i][key], val)) {
              return true;
            } else if (!is_val) {
              return true;
            }
          }
        }
        return false;
      }
      throws(0, "arr_obj_contain", is_arr ? "key" : "arr", is_arr ? "string" : "array");
    }

    /**
     * @method 元素在数组中出现的次数
     * @param  {array}  arr 需要操作的数组
     * @param  {string} ele 要查找的元素
     * @return {number}     元素出现的次数
     *
     * Method.arr_count(arr, ele)
     */
    arr_count(arr, ele) {
      const is_arr = this.data(arr, "array");
      const is_ele = this.data(ele, "string") || this.data(ele, "number");
      if (is_arr && is_ele) {
        let num = 0;
        for (let i = 0; i < arr.length; i++) {
          if (ele === arr[i]) {
            num++;
          }
        }
        return num;
      }
      throws(0, "arr_count", is_arr ? "ele" : "arr", is_arr ? "string/number" : "array");
    }

    /**
     * @method 数组去重
     * @param  {array} arr 需要操作的数组
     * @return {array}     返回去重后的数组
     *
     * Method.arr_del_rep(arr)
     * 去除重复部分，保留唯一值
     */
    arr_del_rep(arr) {
      if (this.data(arr, "array")) {
        return Array.from(new Set(arr));
      }
      throws(0, "arr_del_rep", "arr", "array");
    }

    /**
     * @method 删除数组中某个元素
     * @param  {array}         arr 需要操作的数组
     * @param  {string/number} app 需要删除的字符
     * @param  {boolean}       met 查询方式[已知元素(true)/下标(false)]
     * @return {array}             返回新数组
     *
     * Method.arr_del_ele(arr, app, met)
     * 当通过下标进行查询时，需注意下标是从 0 开始计算
     */
    arr_del_ele(arr, app, met) {
      const is_arr = this.data(arr, "array");
      const is_app = this.data(app, "string") || this.data(app, "number");
      const is_met = this.data(met, "boolean") ? met : true;
      if (is_arr && is_app) {
        for (let i = 0; i < arr.length + 1; i++) {
          let I = arr.indexOf(is_met ? app : arr[i]);
          if (is_met) {
            if (I >= 0) {
              arr.splice(I, 1);
            } else {
              break;
            }
          } else if (this.data(app, "number")) {
            if (app === I) {
              arr.splice(I, 1);
              break;
            }
          }
        }
        return arr;
      }
      throws(0, "arr_del_ele", is_arr ? "app" : "arr", is_arr ? "string/number" : "array");
    }

    /**
     * @method 对比数组取出交集
     * @param  {array} 对比数组，理论上可以无限多个
     * @return {array} 交集数组
     *
     * Method.arr_intersect(arr1, arr2, ……)
     * 若是数组中出现重复的交集，只会取出一例
     * 区分字符串与数字，例 1 与 "1" 将进行区分，并不属于交集
     */
    arr_intersect() {
      let obj = {};
      let result = [];
      for (let i = 0; i < arguments.length; i++) {
        for (let j = 0; j < arguments[i].length; j++) {
          const str = arguments[i][j];
          if (!obj[`${str}_${typeof str}`]) {
            obj[`${str}_${typeof str}`] = 1;
          } else {
            obj[`${str}_${typeof str}`]++;
            if (obj[`${str}_${typeof str}`] === arguments.length) {
              result.push(str);
            }
          }
        }
      }
      return result;
    }

    /**
     * @method 生成SKU
     * @return {array} sku 数组
     *
     * let arr = [[1, 2, 3],[4, 5, 6],[7, 8, 9],[10, 11, 12]]
     * Method.arr_sku(...arr)
     *
     * let arr_1 = [1, 2, 3]
     * let arr_2 = [4, 5, 6]
     * Method.arr_sku(arr_1, arr_2, ……)
     */
    arr_sku() {
      return Array.prototype.reduce.call(arguments, (a, b) => {
        let ret = [];
        a.forEach((a) => {
          b.forEach((b) => {
            ret.push(a.concat([b]));
          });
        });
        return ret;
      }, [[]]);
    }

    // ============================================= 数组操作(结束) =============================================

    // ============================================= 对象操作(开始) =============================================

    /**
     * @method 对象深拷贝
     * @param  {object} obj 需要操作的对象
     * @return {object}     拷贝返回对象
     *
     * Method.obj_deep_copy(obj)
     */
    obj_deep_copy(obj) {
      const result = (Array.isArray(obj) ? [] : this.data(obj, "object") ? {} : obj);
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          if (this.data(obj[key], "object")) {
            result[key] = this.obj_deep_copy(obj[key]);
          } else {
            result[key] = obj[key];
          }
        }
      }
      return result;
    }

    // ============================================= 对象操作(结束) =============================================

    // ============================================= 储存操作(开始) =============================================

    /**
     * @method 设置本地缓存
     * @param  {string}  key     设置缓存名称
     * @param  {*}       val     设置缓存内容
     * @param  {number}  tim     设置缓存有效时间
     * @param  {string}  company 缓存有效时间单位
     * @return {boolean}         返回设置动作布尔值
     *
     * Method.sto_cache_set(key, val, tim, company)
     * 缓存有效时间单位默认为 1/1天
     * 缓存有效时间单位可分:seconds[秒]/minutes[分]/hours[时]/days[天]
     */
    sto_cache_set(key, val, tim, company = "days") {
      const is_key = this.data(key, "string");
      if (is_key) {
        const recombination = {data_main: this.data(val) ? val : null};
        const seconds = +tim;
        if (seconds > 0) {
          let com = "";
          if (company === "seconds") {
            com = 1000;
          } else if (company === "minutes") {
            com = this.num_acc_mul(1000, 60);
          } else if (company === "hours") {
            com = this.num_acc_mul(1000, 60, 60);
          } else if (company === "days") {
            com = this.num_acc_mul(1000, 60, 60, 24);
          }
          recombination.data_time = this.num_acc_add(this.date_get(), this.num_acc_mul(seconds, com));
        }
        localStorage.setItem(key, JSON.stringify(recombination));
        return true;
      }
      throws(0, "sto_cache_set", "key", "string");
    }

    /**
     * @method 获取本地缓存
     * @param  {string}      key 读取缓存名称
     * @return {string/null}     返回获取值或 null 值
     *
     * Method.sto_cache_get(key)
     */
    sto_cache_get(key) {
      if (this.data(key, "string")) {
        const val_local = localStorage.getItem(key);
        if (this.data(val_local)) {
          const val_turn = this.str_is_json(val_local) ? JSON.parse(val_local) : val_local;
          if (val_turn.data_time && (val_turn.data_time < this.date_get())) {
            this.sto_cache_del(key);
            return null;
          }
          return val_turn.hasOwnProperty("data_main") ? val_turn.data_main : val_turn;
        } else {
          return null;
        }
      }
      throws(0, "sto_cache_get", "key", "string");
    }

    /**
     * @method 删除本地缓存
     * @param  {string}  key 删除缓存名称
     * @return {boolean}     返回删除动作布尔值
     *
     * Method.sto_cache_del(key)
     */
    sto_cache_del(key) {
      if (this.data(key, "string")) {
        localStorage.removeItem(key);
        return true;
      }
      throws(0, "sto_cache_del", "key", "string");
    }

    /**
     * @method 设置COOKIE
     * @param  {string}  key  设置cookie名称
     * @param  {*}       val  设置cookie内容
     * @param  {number}  tim  设置cookie有效时间，单位 1/1天
     * @param  {string}  path 作用地址
     * @return {boolean}      返回设置动作布尔值
     *
     * Method.sto_cookie_set(key, val, tim, path)
     */
    sto_cookie_set(key, val, tim, path) {
      const is_key = this.data(key, "string");
      const is_val = this.data(val) && !this.data(val, "function");
      if (is_key && is_val) {
        const date = new Date();
        const seconds = +tim;
        const Val = this.data(val, "array") || this.data(val, "object") ? JSON.stringify(val) : val;
        date.setDate(date.getDate() + seconds);
        doc.cookie = `${key}=${escape(Val)};expires=${date};path=${this.data(key, "string") ? path : "/"}`;
        return true;
      }
      throws(0, "sto_cookie_set", is_key ? "val" : "key", is_key ? "!function" : "string");
    }

    /**
     * @method 获取COOKIE
     * @param  {string}      key 读取cookie名称
     * @return {string/null}     返回获取值或 null 值
     *
     * Method.sto_cookie_get(key)
     */
    sto_cookie_get(key) {
      if (this.data(key, "string")) {
        const arr = doc.cookie.split("; ");
        for (let i = 0; i < arr.length; i++) {
          const arr2 = arr[i].split("=");
          if (arr2[0] === key) {
            const val = unescape(arr2[1]);
            return this.str_is_json(val) ? JSON.parse(val) : val;
          }
        }
        return null;
      }
      throws(0, "sto_cookie_get", "key", "string");
    }

    /**
     * @method 删除COOKIE
     * @param  {string}  key 删除cookie名称
     * @return {boolean}     返回删除动作布尔值
     *
     * Method.sto_cookie_del(key)
     */
    sto_cookie_del(key) {
      if (this.data(key, "string")) {
        this.sto_cookie_set(key, 1, -1);
        return true;
      }
      throws(0, "sto_cookie_del", "key", "string");
    }

    /**
     * @method 设置历史记录
     * @param  {*}       val  需要记录的内容
     * @param  {string}  iden 记录标识
     * @param  {number}  num  最大数量
     * @return {boolean}      返回设置动作布尔值
     *
     * Method.sto_history_set(val, iden, num)
     *
     * 重复的将会被删除并放置第一个位置
     * 历史记录只是在 sto_cache_set 的基础上增加了存入的规则，使其符合与之对应的需求
     */
    sto_history_set(val, iden, num) {
      const is_val = this.data(val);
      if (is_val) {
        const is_iden = this.data(iden, "string") ? iden : "default";
        const is_num = this.data(num, "number") ? +num : 6;

        let history = this.sto_cache_get(`[kxui/method/history/${is_iden}]`) || [];
        if (history.length > 0) {
          for (let i = 0; i < history.length; i++) {
            if (this.data_compare(history[i], val)) {
              history = this.arr_del_ele(history, i, false);
              break;
            }
          }
        }

        history.unshift(val);
        if (history.length > is_num) { history.pop(); }

        this.sto_cache_set(`[kxui/method/history/${is_iden}]`, history);
        return true;
      }
      throws(0, "sto_history_set", "val", "*");
    }

    /**
     * @method 获取历史记录
     * @param  {string} iden 记录标识
     * @return {*}           返回记录的内容
     *
     * Method.sto_history_get(iden)
     */
    sto_history_get(iden) {
      iden = this.data(iden, "string") ? iden : "default";
      return this.sto_cache_get(`[kxui/method/history/${iden}]`);
    }

    /**
     * @method 删除历史记录
     * @param  {string}  iden 记录标识
     * @return {boolean}      返回删除动作布尔值
     *
     * Method.sto_history_del(iden)
     */
    sto_history_del(iden) {
      iden = this.data(iden, "string") ? iden : "default";
      return this.sto_cache_del(`[kxui/method/history/${iden}]`);
    }

    // ============================================= 储存操作(结束) =============================================

    // ============================================= 时间操作(开始) =============================================

    /**
     * @method 获取当前时间戳
     * @param  {boolean} digit 时间戳长度[13位(true)/10位(false)]
     * @return {number}        当前时间戳
     *
     * Method.date_get(digit)
     */
    date_get(digit) {
      const Digit = this.data(digit, "boolean") ? digit : true;
      const time = new Date().getTime();
      const result = (Digit ? time : Number(time.toString().substr(0, 10)));
      return result;
    }

    /**
     * @method 格式化秒[转天时分秒]
     * @param  {number} mss 秒
     * @return {object}     格式化对象
     *
     * Method.date_format_seconds(mss)
     * 只对秒进行格式化，格式单位:1/秒
     */
    date_format_seconds(mss) {
      if (this.data(mss, "number")) {
        const days = Math.floor(this.num_acc_div(mss, this.num_acc_mul(24, 3600)));
        const hours = Math.floor(this.num_acc_div(this.num_acc_sub(mss, this.num_acc_mul(days, 24, 3600)), 3600));
        const minutes = Math.floor(this.num_acc_div(this.num_acc_sub(mss, this.num_acc_mul(days, 24, 3600), this.num_acc_mul(hours, 3600)), 60));
        const seconds = this.num_acc_sub(mss, this.num_acc_mul(days, 24, 3600), this.num_acc_mul(hours, 3600), this.num_acc_mul(minutes, 60));
        return {
          days: this.num_zero_fill(days, 2),
          hours: this.num_zero_fill(hours, 2),
          minutes: this.num_zero_fill(minutes, 2),
          seconds: this.num_zero_fill(seconds, 2)
        };
      }
      throws(0, "date_format_seconds", "mss", "number");
    }

    // ============================================= 时间操作(结束) =============================================

    // ============================================= 数字操作(开始) =============================================

    /**
     * @method 生成随机数字
     * @param  {number} min_num     生成的随机数的最小值[默认(0)]
     * @param  {number} max_num     生成的随机数的最大值[默认(255)]
     * @param  {number} decimal_num 如果想要生成的是带有小数的随机数，则需指定小数点位数
     * @return {string/number}      返回随机结果
     *
     * Method.num_random(min_num, max_num, decimal_num)
     * 当不存在小数时，返回的随机结果数据类型为 number
     * 因为小数原因可能存在 1.00,而在 number 下 1.00 默认还是显示 1，无法满足预期效果，所有返回的数据结构定为 string
     */
    num_random(min_num, max_num, decimal_num) {
      const min = +min_num || 0;
      const max = +max_num || 255;
      const decimal = +decimal_num || 0;
      const differ = max - min;
      const random = Math.random();
      return this.num_decimal(min + differ * random, decimal);
    }

    /**
     * @method 保留任意位小数
     * @param  {number} num 需要操作的数字
     * @param  {number} len 需要保留的位数[不填为舍去小数取整]
     * @return {string}     保留后的数字
     *
     * Method.num_decimal(num, len)
     * 因为小数原因可能存在 1.00,而在 number 下 1.00 默认还是显示 1，无法满足预期效果，所有返回的数据结构定为 string
     */
    num_decimal(num, len) {
      if (this.data(num, "number")) {
        num = +num;
        len = +len || 0;
        if (len > 100) {
          throws(1, "num_decimal", "len", "不能大于 100");
          return false;
        } else if (this.num_decimal_pla(num) >= len) {
          return String(num.toString().match(RegExp(`^\\d+(?:\\.\\d{0,${len !== 0 ? len : length - 1}})?`, "g")));
        } else {
          return num.toFixed(len);
        }
      }
      throws(0, "num_decimal", "num", "number");
    }

    /**
     * @method 获取小数点位数
     * @param  {number} num 需要操作的数字
     * @return {number}     小数点位数
     *
     * Method.num_decimal_pla(num)
     */
    num_decimal_pla(num) {
      if (this.data(num, "number")) {
        num = +num;
        const Y = String(num).indexOf(".") + 1;
        const count = String(num).length - Y;
        if (Y > 0) {
          return count;
        } else {
          return 0;
        }
      }
      throws(0, "num_decimal_pla", "num", "number");
    }

    /**
     * @method 浮点运算[加法]
     * @param  {number} num 参与计算的数字，理论上可以无限多个
     * @return {number}     计算结果
     *
     * Method.num_acc_add(num1, num2, ……)
     */
    num_acc_add() {
      return num_operation(arguments, "add");
    }

    /**
     * @method 浮点运算[减法]
     * @param  {number} num 参与计算的数字，理论上可以无限多个
     * @return {number}     计算结果
     *
     * Method.num_acc_sub(num1, num2, ……)
     */
    num_acc_sub() {
      return num_operation(arguments, "sub");
    }

    /**
     * @method 浮点运算[乘法]
     * @param  {number} num 参与计算的数字，理论上可以无限多个
     * @return {number}     计算结果
     *
     * Method.num_acc_mul(num1, num2, ……)
     */
    num_acc_mul() {
      return num_operation(arguments, "mul");
    }

    /**
     * @method 浮点运算[除法]
     * @param  {number} num 参与计算的数字，理论上可以无限多个
     * @return {number}     计算结果
     *
     * Method.num_acc_div(num1, num2, ……)
     */
    num_acc_div() {
      return num_operation(arguments, "div");
    }

    /**
     * @method 金钱格式
     * @param  {number}  num 需要操作的数字
     * @param  {boolean} pau 是否每隔三位数字添加一个逗号
     * @return {string}      修改过的金钱格式
     *
     * Method.num_money(num, pau)
     */
    num_money(num, pau) {
      if (this.data(num, "number")) {
        num = this.num_decimal(num, 2);
        pau = this.data(pau, "boolean") ? pau : true;
        return (pau ? num.replace(/\d(?=(?:\d{3})+\b)/g, `$&,`) : num);
      }
      throws(0, "num_money", "num", "number");
    }

    /**
     * @method 银行卡号格式
     * @param  {number} num 银行卡号
     * @return {string}     修改过的银行卡号格式
     *
     * Method.num_bank(num)
     */
    num_bank(num) {
      if (this.data(num, "number")) {
        return String(num).replace(/\d(?=(?:\d{4})+\b)/g, `$& `);
      }
      throws(0, "num_bank", "num", "number");
    }

    /**
     * @method 获取百分比
     * @param  {number}  num    需要操作的数字
     * @param  {number}  total  数据总数
     * @param  {boolean} symbol 是否携带百分比单位[携带(true)/不携带(false)]
     * @return {string}         计算后的百分比
     *
     * Method.num_percent(num, total, symbol)
     */
    num_percent(num, total, symbol) {
      const is_num = this.data(num, "number");
      const is_total = this.data(total, "number");
      if (is_num && is_total) {
        const is_symbol = this.data(symbol, "boolean") ? symbol : true;
        const percent_sign = is_symbol ? "%" : "";
        return total <= 0 ? `0${percent_sign}` : `${this.num_acc_div(Math.round(this.num_acc_mul(this.num_acc_div(num, total), 10000)), 100)}${percent_sign}`;
      }
      throws(0, "num_percent", is_num ? "total" : "num", "number");
    }

    /**
     * @method 在某数字前补零
     * @param  {number} num 需要补零的数字
     * @param  {number} len 补零后的总长度
     * @return {string}     补零后的结果
     *
     * Method.num_zero_fill(num, len)
     */
    num_zero_fill(num, len) {
      const is_num = this.data(num, "number");
      const is_len = this.data(len, "number");
      if (is_num && is_len) {
        for(let i = String(num).length; i < len; i = num.length) {
          num = `0${num}`;
        }
        return String(num);
      }
      throws(0, "num_zero_fill", is_num ? "len": "num", "number");
    }

    /**
     * @method 数字变换动画
     * @param  {string/object} dom 需要写入的节点
     * @param  {number}        num 需要参与动画的数字
     *
     * Method.num_ani(dom, num)
     */
    num_ani(dom, num) {
      const is_dom = this.data(dom, "string") ? doc.querySelector(dom) : this.data(dom, "object") ? dom[0] || dom : false;
      const is_num = this.data(num, "number");
      if (is_dom && is_num) {
        // 获取节点的自定义属性
        // 自定义属性用于区分节点差异，并进行存储，确保变换的连续性
        let save_variable = `[${is_dom.tagName}]`;
        for (let i = 0; i < is_dom.attributes.length; i++) { save_variable = `${save_variable}[${is_dom.attributes[i].nodeName}=${is_dom.attributes[i].nodeValue}]`; }
        save_variable = this.str_del_space(save_variable).toLowerCase();

        const is_record = num_ani_list[save_variable];
        const is_decimal = this.num_decimal_pla(num);

        // 初始化创建储存对象
        if (!is_record) { num_ani_list[save_variable] = {timer: "", value: 0}; }

        // 若是存在小数，则在后方补零
        let zero_filling = 1;
        if (is_decimal !== 0) {
          for (let i = 0; i < is_decimal; i++){ zero_filling = `${zero_filling}0`; }
        }

        // 目标数字及初始化赋值
        let pre_number = num_ani_list[save_variable].value;
        const num_target = parseInt(is_decimal === 0 ? num : this.num_acc_mul(num, zero_filling));
        const direction = num_target > pre_number;
        is_dom.innerHTML = pre_number;

        clearInterval(num_ani_list[save_variable].timer);
        num_ani_list[save_variable].timer = setInterval(() => {
          if (parseInt(pre_number) === num_target) {
            num_ani_list[save_variable].value = num_target;
            clearInterval(num_ani_list[save_variable].timer);
          } else {
            const distance = direction ? num_target - pre_number : pre_number - num_target;
            const speed = Math.ceil(this.num_acc_div(distance, 40));
            pre_number = direction ? this.num_acc_add(pre_number, speed) : this.num_acc_sub(pre_number, speed);
            is_dom.innerHTML = (is_decimal === 0 ? pre_number : this.num_acc_div(pre_number, zero_filling));
          }
        }, 5);
      } else {
        throws(0, "num_ani", is_dom ? "num" : "dom", is_dom ? "number" : "string/object");
      }
    }

    /**
     * @method 等比缩放
     * @param  {array}  ori    原始数值
     * @param  {array}  tar    转换数值
     * @param  {number} dec    需要保留的小数位数[默认不保留]
     * @return {string/number} 等比转换后的数值
     *
     * Method.num_zoom([100, 200], ["auto", 200])
     * 因为小数原因可能存在 1.00,而在 number 下 1.00 默认还是显示 1，无法满足预期效果，所有当定义 dec 后，返回的数据结构定为 string
     */
    num_zoom(ori, tar, dec) {
      const is_ori = this.data(ori, "array");
      const is_tar = this.data(tar, "array");
      if (is_ori && is_tar) {
        const arr_count = this.arr_count(tar, "auto");
        if (ori.length !== 2 || tar.length !== 2 || arr_count !== 1) {
          throws(1, "num_zoom", arr_count !== 1 ? "tar" : ori.length === 2 ? "tar" : "ori", "必须且只能存在 1 个 auto 字段");
          return false;
        } else {
          const auto_index = tar.indexOf("auto");
          const percent = this.num_acc_div(this.num_percent(ori[0], ori[1], false), 100);
          const auto_num = auto_index === 0 ? this.num_acc_mul(tar[1], percent) : this.num_acc_div(tar[0], percent);
          return this.data(dec, "number") ? this.num_decimal(auto_num, dec) : auto_num;
        }
      }
      throws(0, "num_zoom", is_ori ? "tar" : "ori", "array");
    }

    // ============================================= 数字操作(结束) =============================================

    // ============================================= 事件操作(开始) =============================================

    /**
     * @method 阻止事件冒泡
     * @param  {object} event 事件
     *
     * Method.evt_stop(event)
     */
    evt_stop(event) {
      const e = event || win.event;
      if (e && e.stopPropagation) {
        e.stopPropagation();
      } else {
        e.cancelBubble = true;
      }
    }

    /**
     * @method 移动端尺寸自适应
     * @param  {number} screen 设计图宽度[默认(750)]
     * @param  {number} delay  延迟时间[默认(300)]
     *
     * Method.evt_font_adapt(screen)
     * 采用淘宝算法进行动态改变 html 尺寸来适配移动端
     * 可根据 screen 参数来对应设计图宽度，例:
     * 已知设计图宽度为 750px 则对应的 rem 单位为:750px = 1rem
     *
     * 延迟时间在切换改变大小时多少时间去重置适配
     * 较小的延迟时间会带来较大的性能消耗
     */
    evt_font_adapt(screen, delay) {
      const lib = win.lib || {};
      const el_doc = doc.documentElement;
      const el_flexible = doc.querySelector("meta[name='flexible']");
      const flexible = lib.flexible || (lib.flexible = {});
      const Screen = +screen || 750;
      const Delay = +delay || 300;

      let el_meta = doc.querySelector("meta[name='viewport']");
      let dpr = 0;
      let scale = 0;
      let tid;

      if (el_meta) {
        const match = el_meta.getAttribute("content").match(/initial\-scale=([\d\.]+)/);
        if (match) {
          scale = parseFloat(match[1]);
          dpr = parseInt(1 / scale);
        }
      } else if (el_flexible) {
        const content = el_flexible.getAttribute("content");
        if (content) {
          const initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
          const maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
          if (initialDpr) {
            dpr = parseFloat(initialDpr[1]);
            scale = parseFloat((1 / dpr).toFixed(2));
          }
          if (maximumDpr) {
            dpr = parseFloat(maximumDpr[1]);
            scale = parseFloat((1 / dpr).toFixed(2));
          }
        }
      }

      if (!dpr && !scale) {
        const is_iPhone = win.navigator.appVersion.match(/iphone/gi);
        const devicePixelRatio = win.devicePixelRatio;
        if (is_iPhone) {
          if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
            dpr = 3;
          } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
            dpr = 2;
          } else {
            dpr = 1;
          }
        } else {
          dpr = 1;
        }
        scale = 1 / dpr;
      }

      el_doc.setAttribute("data-dpr", dpr);
      if (!el_meta) {
        el_meta = doc.createElement("meta");
        el_meta.setAttribute("name", "viewport");
        el_meta.setAttribute("content", `initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}, user-scalable=no`);
        if (el_doc.firstElementChild) {
          el_doc.firstElementChild.appendChild(el_meta);
        } else {
          const wrap = doc.createElement("div");
          wrap.appendChild(el_meta);
          doc.write(wrap.innerHTML);
        }
      }

      const refresh_rem = () => {
        let width = el_doc.getBoundingClientRect().width;
        if (width / dpr > Screen) { width = Screen * dpr; }

        const rem = 100 * (width / Screen);
        el_doc.style.fontSize = `${rem}px`;
        flexible.rem = win.rem = rem;
      };

      win.addEventListener("resize", () => {
        clearTimeout(tid);
        tid = setTimeout(refresh_rem, Delay);
      }, false);

      win.addEventListener("pageshow", (e) => {
        if (e.persisted) {
          clearTimeout(tid);
          tid = setTimeout(refresh_rem, Delay);
        }
      }, false);

      if (doc.readyState === "complete") {
        doc.body.style.fontSize = 12 * dpr + "px";
      } else {
        doc.addEventListener("DOMContentLoaded", () => {
          doc.body.style.fontSize = 12 * dpr + "px";
        }, false);
      }
      refresh_rem();

      flexible.dpr = win.dpr = dpr;
      flexible.refresh_rem = refresh_rem;
      flexible.rem2px = function(d) {
        let val = parseFloat(d) * this.rem;
        if (typeof d === "string" && d.match(/rem$/)) {
          val += "px";
        }
        return val;
      };

      flexible.px2rem = function(d) {
        let val = parseFloat(d) / this.rem;
        if (typeof d === "string" && d.match(/px$/)) {
          val += "rem";
        }
        return val;
      };
    }

    /**
     * @method 移动滚动条位置
     * @param  {string/object} dom      滚动节点[默认(document.documentElement)]
     * @param  {number}        num      滚动距离[默认(0)]
     * @param  {function}      callback 滚动动画结束回调
     *
     * Method.evt_scroll(dom, num, callback)
     */
    evt_scroll(dom, num, callback) {
      const D = this.data(dom, "string") ? doc.querySelector(dom) : this.data(dom, "object") ? dom[0] || dom : false;
      const scrolling = D ? D : doc.documentElement;
      const Y = this.data(dom, "number") ? dom : this.data(num, "number") ? num : 0;
      const C = this.data(callback, "function") ? callback : this.data(num, "function") ? num : this.data(dom, "function") ? dom : false;
      const top = scrolling.scrollTop;
      const direction = Y > top;

      // 结束共同
      const end = () => {
        clearInterval(evt_scroll_timer);
        if (C) { C(); }
      };

      // 滚动距离缓存
      // 当两次滚动距离相同时，表示无法正常滚动至预期目标
      let rolling_distance_cache = "";

      clearInterval(evt_scroll_timer);
      evt_scroll_timer = setInterval(() => {
        const current_y = scrolling.scrollTop;
        const distance = direction ? this.num_acc_sub(Y, current_y) : this.num_acc_sub(current_y, Y);
        const speed = Math.ceil(this.num_acc_div(distance, 40));

        if (parseInt(Y) === parseInt(current_y)) {
          end();
        } else {
          const rolling_distance = direction ? this.num_acc_add(current_y, speed) : this.num_acc_sub(current_y, speed);

          // 当两次滚动距离相同时，表示无法正常滚动至预期目标
          if (rolling_distance_cache === rolling_distance){
            end();
            return false;
          }

          // 缓存滚动距离
          rolling_distance_cache = rolling_distance;

          // 执行页面滚动
          scrolling.scrollTo(0, rolling_distance);
        }
      }, 1);
    }

    /**
     * @method 获取[鼠标/手势]事件位置
     * @param  {object} event 事件
     *
     * Method.evt_location(event)
     */
    evt_location(event) {
      const e = event || win.event;
      let x = 0;
      let y = 0;
      let data = {};

      // 兼容性判断
      if (e.pageX) {
        x = e.pageX;
        y = e.pageY;
      } else if (e.touches && e.touches[0]) {
        x = e.touches[0].clientX;
        y = e.touches[0].clientY;
      } else if (e.changedTouches && e.changedTouches[0]) {
        x = e.changedTouches[0].clientX;
        y = e.changedTouches[0].clientY;
      } else {
        let stop = 0;
        let sleft = 0;
        if (doc.documentElement) {
          stop = doc.documentElement.scrollTop;
          sleft = doc.documentElement.scrollLeft;
        } else {
          stop = doc.body.scrollTop;
          sleft = doc.body.scrollLeft;
        }
        x = e.clientX + sleft;
        y = e.clientY + stop;
      }

      data.x = x;
      data.y = y;
      return data;
    }

    /**
     * @method 获取[鼠标/手势]滑动方向
     * @param  {string/object} dom      [鼠标/手势]触发节点[默认(document.documentElement)]
     * @param  {number}        spa      最小间距[默认(10)]
     * @param  {function}      callback 滑动方向回调
     *
     * Method.evt_direction(dom, spa, callback)
     */
    evt_direction(dom, spa, callback) {
      const C = this.data(callback, "function") ? callback : this.data(spa, "function") ? spa : this.data(dom, "function") ? dom : false;
      if (C) {
        const D = this.data(dom, "string") ? doc.querySelector(dom) : this.data(dom, "object") ? dom[0] || dom : doc.documentElement;
        const S = this.data(spa, "number") ? Number(spa) :  this.data(dom, "number") ? Number(dom) : 10;
        const is_pc = this.data_agent().is_pc;

        // 根据起点终点返回方向[未滑动(0)/上[1]/右[2]/下[3]/左[4]]
        const direction = (start_x, start_y, end_x, end_y) => {
          const ang_x = end_x - start_x;
          const ang_y = end_y - start_y;
          const get_angle = (ang_x, ang_y) => {
            return (Math.atan2(ang_y, ang_x) * 180) / Math.PI;
          };

          // 如果滑动距离太短
          let result = 0;
          if (Math.abs(ang_x) < S && Math.abs(ang_y) < S) {
            return result;
          }

          const angle = get_angle(ang_x, ang_y);
          if (angle >= -135 && angle <= -45) {
            result = 1;
          } else if (angle >= -45 && angle <= 45) {
            result = 2;
          } else if (angle > 45 && angle < 135) {
            result = 3;
          } else if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135)) {
            result = 4;
          }

          return result;
        };

        let start_x = "";
        let start_y = "";
        D.addEventListener(is_pc ? "mousedown" : "touchstart", (event) => {
          start_x = this.evt_location(event).x;
          start_y = this.evt_location(event).y;
        }, false);

        D.addEventListener(is_pc ? "mouseup" : "touchend", (event) => {
          const end_x = this.evt_location(event).x;
          const end_y = this.evt_location(event).y;
          C(direction(start_x, start_y, end_x, end_y));
        }, false);

        return true;
      }
      throws(0, "evt_direction", "callback", "function");
    }

    // ============================================= 事件操作(结束) =============================================

  }

  return new Method();

}));
